
const fs = require('fs');
const path = require('path');
const archiver = require('archiver');
const util = require('./util');
const crypto = require('crypto');

let theType = null;

const tempPath = path.join('..', '.temp')
const srcMd5sJson = path.join(tempPath, 'md5s.json');
const srcMd5sJsonNew = path.join(tempPath, 'md5snew.json');

// Compressed folder, including binary files and JavaScript files.
function compressFolder(thePath, theName) {
  return new Promise((resolve, reject) => {

    let fileContent = JSON.parse(util.readFileInWorkspace('app.dxproj'))
    if (fileContent.components.length === 0) {
      console.log('Components list is empty!');
      return false
    }
    const theNewName = fileContent.name + fileContent.version + '.dpk'

    const inputFolderPath = path.join(thePath, theName);
    let zipName = theType ? theNewName : 'src.zip'
    const outputFilePath = path.join(tempPath, zipName);

    util.deleteZipFile(outputFilePath);
    const output = fs.createWriteStream(outputFilePath);
    const archive = archiver('zip', {
      zlib: { level: 9 } // Set the compression level from 0 to 9, 9 is the highest compression level.
    });

    // Connect the output stream to the compression stream.
    archive.pipe(output);

    // Add folder contents
    archive.directory(inputFolderPath, theType ? 'app/code' : theName);

    // Compression complete, then close the output stream.
    archive.finalize();

    output.on('close', () => {
      resolve(outputFilePath);  // Parsing Promise after compression
    });

    archive.on('warning', (err) => {
      if (err.code === 'ENOENT') {
        console.warn(err.message);
      } else {
        reject(err);  // If a warning appears, reject the Promise.
      }
    });

    archive.on('error', (err) => {
      reject(err);  // If an error occurs, the Promise is rejected.
    });
  });
}

/**
 * Recursive copy folder
 * source Original folder
 * destination target folder
 */
function copyFolderRecursive(source, destination) {
  if (!fs.existsSync(destination)) {
    fs.mkdirSync(destination, { recursive: true });
  }
  if (fs.lstatSync(source).isDirectory()) {

    const fileContent = JSON.parse(util.readFileInWorkspace('app.dxproj'))
    fs.readdirSync(source).forEach((file) => {
      const currentSource = path.join(source, file);
      const currentDestination = path.join(destination, file);

      if (fs.lstatSync(currentSource).isDirectory()) {
        // If the folder name starts with a dot (.), it will not be copied.
        if (file.startsWith('.')) {
          return false;
        }
        // Determine if the filename is included in the ignore list.
        if (fileContent.ignore.folder.indexOf(file) !== -1) {
          return false;
        }
        // If it's a folder, recursively call copyFolderRecursive.
        copyFolderRecursive(currentSource, currentDestination);
      } else {
        // Determine if the filename is included in the ignore list.
        if (fileContent.ignore.file.indexOf(file) !== -1) {
          return false;
        }
        // If it's a file, just copy it.
        fs.copyFileSync(currentSource, currentDestination);
      }
    });
  }
}

/**
   Compare the incremental differences of all files in two folders.

   Iterate through all files in a folder, generate an MD5 hash for each file, and store these MD5 hashes in a JSON file. The file contains a JSON object where the key is the relative path of each file and the value is the MD5 hash. Then, the user might modify the contents of some files in this folder, including adding or deleting files.

   Next, we process this folder again, calculating the MD5 hashes of all files in memory and forming a JSON object. This JSON object is compared with the existing JSON object in the folder.

   If all files exist, compare their MD5 hashes. Finally, we identify the JSON object with the difference and copy all files with the difference to a new temporary directory using their relative paths.
 */
function getSrcRootPath(workspaceRoot, isAll) {
  if (!fs.existsSync(tempPath)) {
    fs.mkdirSync(tempPath, { recursive: true });
  }
  const newMd5s = {}
  traverseFolderToMd5s(workspaceRoot, workspaceRoot, newMd5s);
  const jsonResult = JSON.stringify(newMd5s, null, 2);
  fs.writeFileSync(srcMd5sJsonNew, jsonResult);
  util.emptyFolder(path.join(tempPath, "src"));
  if (isAll || !fs.existsSync(srcMd5sJson)) {
    // If this file is not present, it means this is the first time, so a full sync of the src file will be performed.
    copyFolderRecursive(workspaceRoot, path.join(tempPath, "src"));
  } else {
    const oldMd5s = JSON.parse(fs.readFileSync(srcMd5sJson, 'utf-8'));
    const diff = compareMD5s(oldMd5s, newMd5s);
    for (const key in diff) {
      copyFileAutoMkdir(path.join(workspaceRoot, key), path.join(tempPath, "src", key));
    }
  }
  return tempPath;
}

function ensureDirectoryExistence(filePath) {
  const dirname = path.dirname(filePath);
  if (!fs.existsSync(dirname)) {
    fs.mkdirSync(dirname, { recursive: true });
  }
}

function copyFileAutoMkdir(sourcePath, destinationPath) {
  ensureDirectoryExistence(destinationPath);

  fs.copyFileSync(sourcePath, destinationPath);
  console.log(`File "${sourcePath}" copied to "${destinationPath}"`);
}

function compareMD5s(oldMD5, newMD5) {
  const diff = {};

  for (const key in newMD5) {
    if (!(key in oldMD5) || oldMD5[key] !== newMD5[key]) {
      diff[key] = newMD5[key];
    }
  }
  return diff;
}

function traverseFolderToMd5s(rootPath, folderPath, result) {
  const files = fs.readdirSync(folderPath);

  const fileContent = JSON.parse(util.readFileInWorkspace('app.dxproj'))
  files.forEach(file => {
    const filePath = path.join(folderPath, file);
    if (fs.statSync(filePath).isFile()) {
      // Determine if the filename is included in the ignore list.
      if (fileContent.ignore.file.indexOf(file) !== -1) {
        return false;
      }
      const relativePath = path.relative(rootPath, filePath);
      result[relativePath] = calculateMD5(filePath);
    } else if (fs.statSync(filePath).isDirectory()) {
      // If the folder name starts with a dot (.), no action is taken.
      if (file.startsWith('.')) {
        return false;
      }
      // Determine if the filename is included in the ignore list.
      if (fileContent.ignore.folder.indexOf(file) !== -1) {
        return false;
      }
      traverseFolderToMd5s(rootPath, filePath, result); // Recursive processing subdirectories
    }
  });
}

function calculateMD5(filePath) {
  const content = fs.readFileSync(filePath);
  const md5sum = crypto.createHash('md5');
  md5sum.update(content);
  return md5sum.digest('hex');
}

async function doSync(callback, isAll, type) {
  const workspaceRoot = path.join('../')
  // Get the root directory of the workspace
  if (workspaceRoot) {
    try {
      if (fs.existsSync(tempPath)) {
        await doClear()
      }
      theType = type
      let fileContent = JSON.parse(util.readFileInWorkspace('app.dxproj'))
      let theIgnore = {
        "folder": '.temp,build',
        "file": 'md5s.json,md5snew.json,buildtools.exe'
      }
      let ignoreFlag = false
      if (!fileContent.ignore) {
        ignoreFlag = true
      } else {
        if (!fileContent.ignore.folder && fileContent.ignore.file) {
          theIgnore.file = fileContent.ignore.file
          ignoreFlag = true
        }
        if (!fileContent.ignore.file && fileContent.ignore.folder) {
          theIgnore.folder = fileContent.ignore.folder
          ignoreFlag = true
        }
        if (!fileContent.ignore.file && !fileContent.ignore.folder) {
          ignoreFlag = true
        }
      }
      if (ignoreFlag) {
        fileContent.ignore = theIgnore
        // Write new data
        await fs.writeFileSync(path.join(workspaceRoot, "app.dxproj"), JSON.stringify(fileContent, null, 2));
      }
      let srcRootPath = getSrcRootPath(workspaceRoot, isAll);
      // Compress files under src
      const srcZip = await compressFolder(srcRootPath, "src");
      callback(null, srcZip);
    } catch (error) {
      callback('Component synchronization failed:' + error)
    }
  } else {
    callback('Component synchronization failed: Workspace not open')
  }
}

function doClear() {
  const workspaceRoot = path.join('../')
  theType = null;
  // Get the root directory of the workspace
  if (workspaceRoot) {
    try {
      util.emptyFolder(path.join(tempPath, "src"))
      let zipPath = path.join(tempPath, "src.zip")
      if (fs.existsSync(zipPath)) {
        fs.unlinkSync(zipPath);
      }
    } catch (error) {
      callback('File cleanup failed:' + error)
    }
  } else {
    callback('File cleanup failed: Workspace not open.')
  }
}

module.exports = { doSync, doClear };